import numpy
import random
import shaders
import struct
from OpenGL.GL import *
from gameobjects.color import Color
from gameobjects.vector3 import Vector3
from phuce.gl.buffers import VertexBuffer
from phuce.gl.textures import Texture
from phuce.plane import Plane
from phuce.utils.binary import BinaryFile


def swizzle(xyz):
  return [xyz[0], xyz[2], -xyz[1]]


class BSP46(object):

  def __init__(self):
    self.pvs_frame = None
    self.pvs_leaf = None

  '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
  LOADING FUNCTIONS
  '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

  # the lumps, in they order the appear in the header
  LUMPS = (
    'entities',
    'shaders',
    'planes',
    'nodes',
    'leafs',
    'leaf_surfaces',
    'leaf_brushes',
    'models',
    'brushes',
    'brush_sides',
    'vertexes',
    'indexes',
    'fogs',
    'surfaces',
    'lightmaps',
    'light_volumes',
    'clusters',
    )

  # the byte size of each lump
  SIZEOF = {
    'shaders': 64+4+4,
    'planes': 12+4,
    'nodes': 4+8+12+12,
    'leafs': 4+4+12+12+4+4+4+4,
    'leaf_surfaces': 4,
    'leaf_brushes': 4,
    'models': 12+12+4+4+4+4,
    'brushes': 4+4+4,
    'brush_sides': 4+4,
    'indexes': 4,
    'fogs': 64+4+4,
    'surfaces': 4+4+4+4+4+4+4+4+8+8+12+24+12+8,
    'lightmaps': 128*128*3,
    'light_volumes': 3+3+2,
    }

  def read(self, file, close=True):
    '''
    Load the BSP from the file.
    '''
    file = BinaryFile(file)
    assert file.read_bytes(4) == 'IBSP'
    assert file.read('<i') == 46
    offset, length = {}, {}
    for lump in self.LUMPS:
      offset[lump] = file.read('<i')
      length[lump] = file.read('<i')
    range_for_lump = lambda lump: range(length[lump] / self.SIZEOF[lump])
    seek = lambda lump: file.seek(offset[lump])
    for lump in self.LUMPS:
      file.seek(offset[lump])
      func = getattr(self, '_read_' + lump)
      if lump in self.SIZEOF:
        objects = [func(file) for i in range(length[lump] / self.SIZEOF[lump])]
      else:
        objects = func(file, length[lump])
      setattr(self, lump, objects)
    self._relationships()
    if close:
        file.close()
    return self

  def _read_brushes(self, file):
    return Brush().read(file)

  def _read_brush_sides(self, file):
    return BrushSide().read(file)

  def _read_clusters(self, file, length):
    num_clusters = file.read('<i')
    bytes_per_cluster = file.read('<i')
    bitmasks = [file.read_bytes(bytes_per_cluster) for i in range(num_clusters)]
    clusters = [Cluster() for i in range(num_clusters)]
    for i, cluster in enumerate(clusters):
      clusters[i].index = i
      clusters[i].bitmask = [ord(bitmask) for bitmask in bitmasks[i]]
    return clusters

  def _read_entities(self, file, length):
    return file.read_string(length)

  def _read_fogs(self, file):
    return Effect().read(file)

  def _read_indexes(self, file):
    return file.read('<I')

  def _read_leafs(self, file):
    return Leaf().read(file)

  def _read_leaf_brushes(self, file):
    return file.read('<i')

  def _read_leaf_surfaces(self, file):
    return file.read('<i')

  def _read_lightmaps(self, file):
    return Lightmap().read(file)

  def _read_light_volumes(self, file):
    return LightVolume().read(file)

  def _read_models(self, file):
    return Model().read(file)

  def _read_nodes(self, file):
    return Node().read(file)

  def _read_planes(self, file):
    plane = Plane()
    plane.normal = Vector3(*swizzle(file.read('<fff')))
    plane.distance = file.read('<f')
    plane.type = Plane.type_for_normal(plane.normal)
    return plane

  def _read_shaders(self, file):
    return SurfaceShader().read(file)

  def _read_surfaces(self, file):
    # this is so annoying
    # why couldn't id put the type first
    classes = {1: BrushSurface, 2: PatchSurface, 3: TriangleSurface, 4: BillboardSurface}
    _, _, type = file.read('<iii')
    file.seek(-12, whence=1)
    return classes[type]().read(file, self.indexes)

  def _read_vertexes(self, file, length):
    length = length / (12+8+8+12+4)
    vertexes = {'xyz': [], 'st': [], 'stlm': [], 'normal': [], 'rgba': []}
    for i in range(length):
      vertexes['xyz'].append(swizzle(file.read('<fff')))
      vertexes['st'].append(file.read('<ff'))
      vertexes['stlm'].append(file.read('<ff'))
      vertexes['normal'].append(swizzle(file.read('<fff')))
      vertexes['rgba'].append([ord(b) / 255.0 for b in file.read_bytes(4)])
    for k in vertexes:
      vertexes[k] = VertexBuffer(numpy.array(vertexes[k], dtype=numpy.float32), GL_STATIC_DRAW)
    return vertexes

  '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
  RELATIONSHIP MAPPING
  '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

  RELATIONSHIPS = (
    # tuples instead of dict because processing order is important
    ('brushes', {'sides': 'brush_sides', 'shader': 'shaders'}),
    ('brush_sides', {'plane': 'planes', 'shader': 'shaders'}),
    ('fogs', {'brush': 'brushes'}),
    ('leaf_brushes', 'brushes'),
    ('leaf_surfaces', 'surfaces'),
    ('leafs', {'cluster': 'clusters', 'surfaces': 'leaf_surfaces', 'brushes': 'leaf_brushes'}),
    ('models', {'surfaces': 'surfaces', 'brushes': 'brushes'}),
    ('nodes', {'plane': 'planes'}),
    ('surfaces', {'shader': 'shaders', 'fog': 'fogs', 'lightmap': 'lightmaps'}),
    )

  def _relationships(self):
    '''
    Use BSP46.RELATIONSHIPS to change integer indexes into
    references to the related objects. If the attribute is
    an int, it represents a single reference. If it's a
    list, it represents a relative range of indexes.

    e.g.
    surface.shader => bsp.surfaces[suface.shader]
    surface.vertexes => bsp.vertexes[surface.vertexes[0]:surface.vertexes[0]+surface.vertexes[1]]

    Also handles the node children special case, where
    negative indexes must be converted to point to leafs.
    '''
    for relationship in self.RELATIONSHIPS:
      lump, related = relationship
      lump_items = getattr(self, lump)
      if isinstance(related, str):
        related_items = getattr(self, related)
        setattr(self, lump, [related_items[i] for i in lump_items])
        continue
      for item in lump_items:
        for attr, related_lump in related.iteritems():
          value = getattr(item, attr)
          if value == None:
            continue
          related_items = getattr(self, related_lump)
          try:
            if isinstance(value, int):
              value = related_items[value]
            else:
              value = related_items[value[0]:value[0]+value[1]]
          except IndexError:
            raise IndexError, '%s[%d].%s is out of range (%d, len(%s) == %d)' % (lump, lump_items.index(lump), attr, value, related_lump, len(related_items))
          setattr(item, attr, value)
    # set children for each node
    for node in self.nodes:
      for i, child in enumerate(node.children):
        node.children[i] = child < 0 and self.leafs[-(child + 1)] or self.nodes[child]
    # set parents for each node/leaf
    def walk(node, parent, level):
      node.parent = parent
      if node.is_leaf:
        return
      walk(node.children[0], node, level + 1)
      walk(node.children[1], node, level + 1)
    walk(self.nodes[0], None, 0)
    for shader in self.shaders:
        shader.load()

  '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
  RENDERING FUNCTIONS
  '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

  def leaf_for_point(self, point):
    '''
    Traverses the BSP tree to find which leaf the point is in.
    '''
    node = self.nodes[0]
    while not node.is_leaf:
      node = node.children[node.plane.side(point) == Plane.FRONT and 0 or 1]
    return node

  def render(self, view):
    view.apply()
    surfaces = self.surfaces_visible_from(view)
    self.vertexes['xyz'].bind_vertexes(3, GL_FLOAT)
    self.vertexes['normal'].bind_normals(GL_FLOAT)
    self.vertexes['stlm'].bind_texcoords(2, GL_FLOAT)
    indexes = []
    chunks = {}
    for surface in surfaces:
      if not chunks.has_key(surface.lightmap):
        chunks[surface.lightmap] = []
      chunks[surface.lightmap].append(surface)
    glEnable(GL_TEXTURE_2D)
    for chunk in chunks.itervalues():
      indexes = []
      for surface in chunk:
        indexes.extend(surface.indexes)
      surface.lightmap.texture.bind()
      glDrawElementsui(GL_TRIANGLES, numpy.array(indexes, dtype=numpy.uint32))

  def surfaces_visible_from(self, view):
    '''
    Return a list of all the leafs which are potentially visible from the point.
    '''
    from_leaf = self.leaf_for_point(view.orientation.xyz)
    if from_leaf != self.pvs_leaf:
      self.pvs_leaf = from_leaf
      if self.clusters and from_leaf.cluster:
        for leaf in self.leafs:
          if leaf.cluster and leaf.cluster.is_visible_from(from_leaf.cluster):
            self.pvs_surfaces.extend(leaf.surfaces)
        self.pvs_surfaces = list(set(self.pvs_surfaces))
      else:
        self.pvs_surfaces = self.surfaces
    return [surface for surface in self.pvs_surfaces if not surface.cull(view)]

  '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
  TRACING FUNCTIONS
  '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

  TRACE_EPSILON = 0.125

  def trace(self, **kwargs):
    self.tr = Trace(**kwargs)
    self.trace_into_node(self.nodes[0], 0.0, 1.0, self.tr.start, self.tr.end)
    if self.tr.fraction < 1.0:
      self.tr.end = self.tr.start + (self.tr.end - self.tr.start) * self.tr.fraction
    return self.tr

  def trace_into_brush(self, brush):
    if not brush.sides:
      return
    start_fraction, end_fraction = -1.0, 1.0
    start_out, end_out = False, False
    plane = None
    for side in brush.sides:
      sd = side.plane.distance_to(self.tr.start)
      ed = side.plane.distance_to(self.tr.end)
      # see if the end points are in front of the plane
      if sd > 0:
        start_out = True
      if ed > 0:
        end_out = True
      # skip the brush if both are in front of the plane
      if sd > 0 and (ed >= self.TRACE_EPSILON or ed >= sd):
        return
      # skip the side if both are behind the plane
      if sd <= 0 and ed <= 0:
        continue
      if sd > ed: # entering the brush
        f = max((sd - self.TRACE_EPSILON) / (sd - ed), 0)
        if f > start_fraction:
          start_fraction = f
          plane = side.plane
      else: # leaving the brush
        f = min((sd + self.TRACE_EPSILON) / (sd - ed), 1)
        if f < end_fraction:
          end_fraction = f
    if not start_out:
      self.tr.starts_solid = True
      if not end_out:
        self.tr.all_solid = True
        self.tr.fraction = 0
      return
    if start_fraction < end_fraction and start_fraction > -1 and start_fraction < self.tr.fraction:
      self.tr.fraction = max(start_fraction, 0)
      self.tr.plane = plane

  def trace_into_node(self, node, start_fraction, end_fraction, start, end):
    if node.is_leaf:
      for brush in node.brushes:
        if brush.shader and brush.shader.content_flags & self.tr.contents_mask:
          self.trace_into_brush(brush)
          if self.tr.fraction == 0:
            return
      return
    sd = node.plane.distance_to(start)
    ed = node.plane.distance_to(end)
    if sd >= 1 and ed >= 1:
      self.trace_into_node(node.children[0], start_fraction, end_fraction, start, end)
    elif sd < -1 and ed < -1:
      self.trace_into_node(node.children[1], start_fraction, end_fraction, start, end)
    else:
      if sd > ed:
        side = 0
        fraction1 = (sd + self.TRACE_EPSILON) / (sd - ed)
        fraction2 = (sd + self.TRACE_EPSILON) / (sd - ed)
      elif sd < ed:
        side = 1
        fraction1 = (sd + self.TRACE_EPSILON) / (sd - ed)
        fraction2 = (sd - self.TRACE_EPSILON) / (sd - ed)
      else:
        side = 0
        fraction1 = 1
        fraction2 = 0
      fraction1 = max(0, min(1, fraction1))
      fraction2 = max(0, min(1, fraction2))
      middle = start + (end - start) * fraction1
      middle_fraction = start_fraction + (end_fraction - start_fraction) * fraction1
      self.trace_into_node(node.children[side], start_fraction, middle_fraction, start, middle)
      middle = start + (end - start) * fraction2
      middle_fraction = start_fraction + (end_fraction - start_fraction) * fraction2
      self.trace_into_node(node.children[side^1], middle_fraction, end_fraction, middle, end)


class Brush(object):
  ''' Brushes are convex hulls used for BSP collision detection. '''
  def read(self, file):
    self.sides = file.read('<ii')
    self.shader = file.read('<i')
    return self


class BrushSide(object):
  ''' One side (plane) of a brush. '''
  def read(self, file):
    self.plane = file.read('<i')
    self.shader = file.read('<i')
    return self


class Cluster(object):
  ''' Chunk of the visibility data. '''
  def is_visible_from(self, cluster):
    return not cluster or bool(self.bitmask[cluster.index >> 3] & (1 << (cluster.index & 7)))


class Effect(object):
  def read(self, file):
    self.name = file.read_string(64)
    self.brush = file.read('<i')
    self.visible_side = file.read('<i');
    return self


class Leaf(object):
  ''' A node without any children, the end of a branch in the tree. '''

  is_leaf = True
  is_node = False

  def __init__(self):
    self.pvs_frame = None

  def read(self, file):  
    self.cluster = file.read('<i')
    if self.cluster == -1:
      self.cluster = None
    self.area = file.read('<i')
    self.mins = Vector3(*swizzle(file.read('<fff')))
    self.maxs = Vector3(*swizzle(file.read('<fff')))
    self.surfaces = file.read('<ii')
    self.brushes = file.read('<ii')
    return self


class Lightmap(object):
  '''
  Lightmaps are special textures used to simulate lighting on the surfaces.
  The BSP compiler does heavy duty radiosity calculations, and stores the
  results in these texture maps.
  '''

  def __init__(self):
    self.texture = Texture()
    self.texture.set_options({
      GL_TEXTURE_WRAP_S: GL_CLAMP,
      GL_TEXTURE_WRAP_T: GL_CLAMP,
      GL_TEXTURE_MAG_FILTER: GL_LINEAR,
      GL_TEXTURE_MIN_FILTER: GL_LINEAR,
    })

  def read(self, file):
    self.buffer = file.read_bytes(128*128*3)
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 128, 128, 0, GL_RGB, GL_UNSIGNED_BYTE, self.buffer)
    return self


class LightVolume(object):
  '''
  The BSP is divided into a 3D grid, and the light is calculated throughout
  that grid. Light volumes represent the lighting information for an individual
  cell in the grid.
  '''
  def read(self, file):
    self.ambient = Color(*[ord(b) / 255.0 for b in file.read_bytes(3)])
    self.directional = Color(*[ord(b) / 255.0 for b in file.read_bytes(3)])
    self.normal = file.read_bytes(2)
    return self


class Model(object):
  '''
  Models represent a collection of brushes and surfaces in the BSP. The first
  model represents the data for the BSP tree. The rest of the models are for
  other BSP entities in the world, like doors and elevators. These additional
  models do not have a BSP tree associated with them.
  '''
  def read(self, file):
    self.mins = Vector3(*swizzle(file.read('<fff')))
    self.maxs = Vector3(*swizzle(file.read('<fff')))
    self.surfaces = file.read('<ii')
    self.brushes = file.read('<ii')
    return self


class Node(object):
  '''
  The BSP tree is a set of nodes. Each node has an associated splitting plane,
  and two children -- one child for everything on the front side of the plane,
  another child for everything behind the plane.
  '''

  is_leaf = False
  is_node = True

  def __init__(self):
    self.pvs_frame = None

  def read(self, file):
    self.plane = file.read('<i')
    self.children = file.read('<ii')
    self.mins = Vector3(*swizzle(file.read('<fff')))
    self.maxs = Vector3(*swizzle(file.read('<fff')))
    return self


class Surface(object):
  ''' This is a generic class used as a base for the BSP surfaces. '''

  def cull(self, view):
    return False

  def read(self, file, indexes):
    self.shader = file.read('<i')
    self.fog = file.read('<i')
    if self.fog == -1:
      self.fog = None
    type = file.read('<i')
    first_vertex, num_vertexes = file.read('<ii')
    first_index, num_indexes = file.read('<ii')
    self.indexes = [indexes[i] + first_vertex for i in range(first_index, first_index+num_indexes)]
    self.lightmap = file.read('<i')
    self.lightmapping = {}
    self.lightmapping['x'] = file.read('<i')
    self.lightmapping['y'] = file.read('<i')
    self.lightmapping['w'] = file.read('<i')
    self.lightmapping['h'] = file.read('<i')
    self.lightmapping['origin'] = Vector3(*swizzle(file.read('<fff')))
    self.lightmapping['normals'] = [Vector3(*swizzle(file.read('<fff'))) for i in range(2)]
    self.normal = Vector3(*swizzle(file.read('<fff')))
    self.patch = {}
    self.patch['width'] = file.read('<i')
    self.patch['height'] = file.read('<i')
    return self

class BillboardSurface(Surface):
  ''' An axis aligned surface, used for sprite effects and the like. '''
  pass

class BrushSurface(Surface):
  ''' Normal, planar BSP surface. '''
  def cull(self, view):
    #FIXME: shaders
    #if self.shader.cull.two_sided:
    #  return False
    #d = self.plane.normal.dot(view.orientation.xyz) - self.plane.distance
    #if shader.cull.front_sided:
    #  return d < -8
    #else:
    #  return d > 8
    return False

class PatchSurface(Surface):
  ''' Bezier patch surface. '''
  def cull(self, view):
    return False

class TriangleSurface(Surface):
  ''' Triangle soup. '''
  def cull(self, view):
    return False


class SurfaceShader(object):
  '''
  Shaders are Quake 3's material system. Although there are shader script
  files, which must be loaded independently of the BSP, the BSP also contains
  a shader lump for associating BSP surfaces with the shader script names.
  '''
  def read(self, file):
    self.shader = file.read_string(64)
    self.surface_flags = file.read('<i')
    self.content_flags = file.read('<i')
    return self

  def load(self):
    self.shader = shaders.load(self.shader)


class Trace(object):
  ''' 
  This object doesn't represent a BSP lump, but is used to return the results
  of a trace against the BSP tree.
  '''
  def __init__(self, start, end, contents_mask=None):
    self.start = start.copy()
    self.end = end.copy()
    self.fraction = 1.0
    self.all_solid = False
    self.starts_solid = False
    self.contents_mask = contents_mask

  def __str__(self):
    return "Trace[from %s to %s (%f of total)]" % (self.start, self.end, self.fraction)
